/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
var ConcatSource = require("webpack-core/lib/ConcatSource");
var TemplateArgumentDependency = require("../dependencies/TemplateArgumentDependency");

function DedupePlugin() {
}
module.exports = DedupePlugin;

DedupePlugin.prototype.apply = function(compiler) {
	compiler.plugin("compilation", function(compilation) {

		compilation.dependencyTemplates.set(TemplateArgumentDependency, new TemplateArgumentDependency.Template());

		compilation.plugin("after-optimize-modules", function(modules) {
			var modulesByHash = {};
			var allDups = [];
			modules.forEach(function(module, idx) {
				if(!module.getSourceHash || !module.getAllModuleDependencies || !module.createTemplate || !module.getTemplateArguments) return;
				var hash = module.getSourceHash();
				var dupModule = modulesByHash[hash];
				if(dupModule) {
					if(dupModule.duplicates) {
						dupModule.duplicates.push(module);
						module.duplicates = dupModule.duplicates;
					} else {
						allDups.push(module.duplicates = dupModule.duplicates = [dupModule, module]);
					}
				} else {
					modulesByHash[hash] = module;
				}
			});
		});
		compilation.plugin("after-optimize-chunks", function(chunks) {
			var entryChunks = chunks.filter(function(c) { return c.entry; });
			entryChunks.forEach(function(chunk) { // for each entry chunk
				var hasDeduplicatedModules = false;
				(function x(dups, roots, visited, chunk) {
					var currentDups = [];
					var currentRoots = [];
					chunk.modules.forEach(function(module) {
						if(module.duplicates) {
							if(!module.rootDuplicatesChunks)
								module.rootDuplicatesChunks = module.chunks.slice();
							var chunkIndex = module.rootDuplicatesChunks.indexOf(chunk);
							if(!module.rootDuplicates) module.rootDuplicates = [];
							var idx = currentDups.indexOf(module.duplicates);
							if(idx >= 0) {
								module.rootDuplicates[chunkIndex] = currentRoots[idx];
								module.rootDuplicates[chunkIndex].push(module);
								module.rootDuplicates[chunkIndex].commonModules =
									mergeCommonModules(module.rootDuplicates[chunkIndex].commonModules, module.getAllModuleDependencies());
								hasDeduplicatedModules = true;
							} else {
								idx = dups.indexOf(module.duplicates);
								if(idx < 0) {
									module.rootDuplicates[chunkIndex] = [module];
									module.rootDuplicates[chunkIndex].commonModules = module.getAllModuleDependencies();
									module.rootDuplicates[chunkIndex].initialCommonModulesLength = module.rootDuplicates[chunkIndex].commonModules.length;
									dups = dups.concat([module.duplicates]);
									roots = roots.concat([module.rootDuplicates[chunkIndex]]);
									currentDups = currentDups.concat([module.duplicates]);
									currentRoots = currentRoots.concat([module.rootDuplicates[chunkIndex]]);
								} else {
									module.rootDuplicates[chunkIndex] = roots[idx];
									module.rootDuplicates[chunkIndex].commonModules =
										mergeCommonModules(module.rootDuplicates[chunkIndex].commonModules, module.getAllModuleDependencies());
									hasDeduplicatedModules = true;
								}
							}
						}
					});
					chunk.chunks.forEach(function(chunk) {
						if(visited.indexOf(chunk) < 0)
							x(dups, roots, visited.concat(chunk), chunk);
					})

					currentRoots.forEach(function(roots) {
						var commonModules = roots.commonModules;
						var initialLength = roots.initialCommonModulesLength;
						if(initialLength !== commonModules.length) {
							var template = roots[0].createTemplate(commonModules);
							roots.template = template;
							chunk.addModule(template);
							template.addChunk(chunk);
							compilation.modules.push(template);
							hasDeduplicatedModules = true;
						}
					});
				}([], [], [], chunk));
				if(hasDeduplicatedModules)
					chunk._DedupePlugin_hasDeduplicatedModules = true;
			});
		});
		function mergeCommonModules(commonModules, newModules) {
			return commonModules.filter(function(module) {
				return newModules.indexOf(module) >= 0;
			});
		}
	});
	compiler.moduleTemplate = new DedupModuleTemplateDecorator(compiler.moduleTemplate);
	compiler.mainTemplate = Object.create(compiler.mainTemplate);
	var oldRenderAddModule = compiler.mainTemplate.renderAddModule;
	compiler.mainTemplate.renderAddModule = function(hash, chunk, varModuleId, varModule) {
		if(!chunk._DedupePlugin_hasDeduplicatedModules) {
			return oldRenderAddModule.call(this, hash, chunk, varModuleId, varModule);
		}
		return [
			"var _m = " + varModule + ";",
			"",
			"// Check if module is deduplicated",
			"switch(typeof _m) {",
			"case \"number\":",
			this.indent([
				"// Module is a copy of another module",
				"modules[" + varModuleId + "] = modules[_m];",
				"break;"
			]),
			"case \"object\":",
			this.indent([
				"// Module can be created from a template",
				"modules[" + varModuleId + "] = (function(_m) {",
				this.indent([
					"var args = _m.slice(1), fn = modules[_m[0]];",
					"return function (a,b,c) {",
					this.indent([
						"fn.apply(null, [a,b,c].concat(args));"
					]),
					"};"
				]),
				"}(_m));",
				"break;"
			]),
			"default:",
			this.indent([
				"// Normal module",
				"modules[" + varModuleId + "] = _m;"
			]),
			"}"
		]
	};
	var oldRenderModules = compiler.mainTemplate.renderModules;
	compiler.mainTemplate.renderModules = function renderModules(hash, chunk, moduleTemplate, dependencyTemplates) {
		if(!chunk._DedupePlugin_hasDeduplicatedModules) {
			return oldRenderModules.call(this, hash, chunk, moduleTemplate, dependencyTemplates);
		}
		var source = new ConcatSource();
		source.add("(function(modules) {\n");
		source.add(this.indent([
			"// Check all modules for deduplicated modules",
			"for(var i in modules) {",
			this.indent([
				"switch(typeof modules[i]) {",
				"case \"number\":",
				this.indent([
					"// Module is a copy of another module",
					"modules[i] = modules[modules[i]];",
					"break;"
				]),
				"case \"object\":",
				this.indent([
					"// Module can be created from a template",
					"modules[i] = (function(_m) {",
					this.indent([
						"var args = _m.slice(1), fn = modules[_m[0]];",
						"return function (a,b,c) {",
						this.indent([
							"fn.apply(null, [a,b,c].concat(args));"
						]),
						"};"
					]),
					"}(modules[i]));"
				]),
				"}"
			]),
			"}",
			"return modules;"
		]));
		source.add("\n}(");
		source.add(oldRenderModules.call(this, hash, chunk, moduleTemplate, dependencyTemplates));
		source.add("))");
		return source;
	};
};

function DedupModuleTemplateDecorator(template) {
	this.template = template;
}

DedupModuleTemplateDecorator.prototype.render = function(module, dependencyTemplates, chunk) {
	if(!module.rootDuplicatesChunks || !chunk) return this.template.render(module, dependencyTemplates, chunk);
	var chunkIndex = module.rootDuplicatesChunks.indexOf(chunk);
	if(!module.rootDuplicates || !module.rootDuplicates[chunkIndex]) return this.template.render(module, dependencyTemplates, chunk);
	var rootDuplicates = module.rootDuplicates[chunkIndex];
	if(rootDuplicates.template) {
		rootDuplicates.template.addReason(module, {
			type: "template",
			request: module.request,
			templateModules: rootDuplicates.template.templateModules
		});
		rootDuplicates.template.reasons.sort(function(a, b) {
			if(a.request == b.request) return 0;
			return a.request < b.request ? -1 : 1;
		});
		var array = [rootDuplicates.template.id].concat(module.getTemplateArguments(rootDuplicates.template.templateModules).map(function(module) {
			if(typeof module.id !== "number")
				return "(function webpackMissingModule() { throw new Error(" + JSON.stringify("Cannot find module") + "); }())"
			return module.id;
		}));
		var source = new ConcatSource("[" + array.join(", ") + "]");
		return source;
	} else {
		rootDuplicates.sort(function(a, b) {
			return a.id - b.id;
		});
		if(module === rootDuplicates[0]) return this.template.render(module, dependencyTemplates, chunk);
		var source = new ConcatSource("" + rootDuplicates[0].id);
		return source;
	}
};

DedupModuleTemplateDecorator.prototype.updateHash = function(hash) {
	hash.update("DedupModuleTemplateDecorator");
	this.template.updateHash(hash);
};